昨天拿到登入憑證了,今天正式打開 Garmin 錶同步上去的那個東西—FIT 檔案。
我們可以知道 FIT 不是 JSON,打開來看不到任何一個你認得的字。它是 Garmin 自己的二進位格式,一整包全是位元組。好消息是不用自己手拆—Python 的 fitdecode 已經幫忙做完解碼,我們要做的是搞懂解出來之後裡面到底裝了什麼。
拿一份不大的檔案練手—8 月 13 日那場 1600m×3 的間歇,跑起來 28 分鐘。把裡面所有的訊息類型跟出現次數印出來:
record 1685
unknown_534 1685
unknown_325 294
time_in_zone 52
split 36
unknown_326 36
lap 15
device_info 12
unknown_104 6
unknown_327 5
28 分鐘的訓練,record 出現了 1685 次—差不多每秒一筆,這是整份檔案的主軸,逐秒紀錄了自己跑步當下的每一個瞬間。lap 有 15 筆,對應這場訓練被切成的 15 個分段(暖身、三趟間歇、每趟之間的恢復跑,各自算一段,其實就是按錶的按鈕 Lap 的切割)。
比較嚇人的是那排 unknown_*。unknown_534 出現次數跟 record一模一樣(1685 次),代表它也是逐秒紀錄的—但這邊無法知道裡面裝什麼。這些是 Garmin 自己裝置的私有欄位,公開的 FIT SDK 沒收錄定義,連 fitdecode 都只能標成「無法辨識」,原封不動印出代號。
呼應昨天那個查不到型號的裝置代碼:這份檔案裡部分的訊息,是連公開文件都無法確認的內容。能用的只有那些有明確定義的訊息類型,剩下的就先放著。
好消息是,要重建一場訓練的樣貌,不需要搞懂全部—只要抓住三種:
record—每秒一筆的原始紀錄。 這場間歇裡,一筆 record 長這樣(拿掉了其餘 22 個值是 None 的欄位):
timestamp 2026-08-13 12:03:53
position_lat 298858368
position_long 1450173354
distance 0.0
heart_rate 117
enhanced_speed 0.0
enhanced_altitude 6.8
power 32
這遠比昨天貼過的摘要 API 豐富—完整版的 record 有 30 個欄位,除了心率配速,還有功率、垂直振幅、觸地時間這些跑步動態數據。這些指標 Garmin Connect App 都看得到,但看到的是整理過的圖表跟平均值;FIT 檔給你的是每一秒的原始數字。
差別在於,原始數字是可以拿去用的:上面這筆裡的 heart_rate 跟 enhanced_speed,Day 22 會變成復盤圖上疊在一起的兩條曲線,用來看心率怎麼在配速不變的情況下一路往上爬。至於功率、垂直振幅這些,目前雖然沒用上—但它們都在,之後可以再看看怎麼呈現在儀表板上。
lap—分段摘要。 每一次你按錶分段(或系統自動分段),就是一筆 lap,裡面存著這一段的距離、時間、平均心率。
session—整場總結。 一份檔案通常只有一筆,裝的是這整場訓練的總覽:
sport running
total_distance 5220.0
total_timer_time 1683.943
avg_heart_rate 170
max_heart_rate 191
這三種訊息疊起來,就是一場訓練從頭到尾的完整故事:session 就是「跑了什麼」、lap 就是「分段在哪」、record 告訴你「每一秒發生了什麼」。
上面那筆 record 裡,position_lat 的值是 298858368。欄位名稱寫著 lat,所以它確實是緯度,但緯度的範圍就是 -90 到 +90,兩億九千萬顯然不在裡面。
這邊是單位上的差異。FIT 存座標用的是 semicircle(半圓),不是我們熟悉的度數。換算公式是:
degrees = raw_value * (180 / 2**31)
拿真實數字驗證:
>>> 298858368 * (180 / 2**31)
25.050019
>>> 1450173354 * (180 / 2**31)
121.552126
換出來是 25.050019, 121.552126 即是這個訓練的地點—松山區田徑場的座標。不知道這個公式的話,直接拿 298858368 去畫地圖,應該會滿頭問號^0^
今天用的是一份 28 分鐘的小檔案,訊息種類的分佈一眼看得完。明天要換上真正的大檔案—一場 3 小時 22 分的全馬,一萬兩千多筆 record。這篇建立的框架(三種訊息、座標換算)會直接拿去用,重建出完整的分段配速表-來看這場全馬到底是在哪一公里開始崩潰的...